﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace DP.RecallDeal.Interface.ObjectTransformer
{
    public class ObjectTranformerService : IObjectTranformerService
    {
        /// <summary>
        /// This property presents the map between source object's properties and distination object's properties
        /// 
        /// </summary>
        public IList<KeyValuePair<string, string>> Mappings { get; set; }

        public ObjectTranformerService(IList<KeyValuePair<string, string>> mappings)
        {
            this.Mappings = mappings;
        }

        public ObjectTranformerService()
        {

        }

        public IList<MappingException> MappingExceptions 
        {
            get { return mappingExceptions; }
        }

        IList<MappingException> mappingExceptions = new List<MappingException>();

        /// <summary>
        /// Base on Mapping properties, assign Source property values to Destination property values
        /// </summary>
        /// <param name="srcObject">Source object</param>
        /// <param name="targetObject">Target object</param>
        /// <returns>List of MappingException, contains </returns>
        public virtual void TransformObject(object srcObject, object targetObject) 
        {
            mappingExceptions.Clear();
            
            foreach (KeyValuePair<string, string> map in Mappings)
            {
                object sourcePropertyValue = GetValue(srcObject, map.Key);
                if (sourcePropertyValue != null)
                    FillObject(map.Value, targetObject, sourcePropertyValue);

                    //MappingException mappingException = SetValue(map, srcObject, targetObject);
                    //if (mappingException != null)
                    //    mappingExceptions.Add(mappingException);

            }
        }        

        /// <summary>
        /// This method is used to fill value to object's property in the chain
        /// </summary>
        /// <param name="propertiesChain">Target property will be filled by value. Would be able to have the form of Parent.Child.GrandChild</param>
        /// <param name="targetInstance">Object instance</param>
        /// <param name="sourceValue">Desirable value will be filled to target instance's property</param>
        public virtual void FillObject(string propertiesChain, object targetInstance, object sourceValue)
        {
            if (targetInstance == null || sourceValue == null || string.IsNullOrEmpty(propertiesChain))
                return;

            string[] properties = propertiesChain.Split('.');
            string property = properties[0];            

            try
            {

                PropertyInfo propertyInfo = targetInstance.GetType().GetProperty(property);

                if (properties.Length > 1) //It has child property
                {
                    object propertyInstance = propertyInfo.GetValue(targetInstance, null);

                    if (propertyInstance == null)
                    {
                        propertyInstance = Activator.CreateInstance(propertyInfo.PropertyType);
                        CastAndSetValue(propertyInfo, targetInstance, propertyInstance);
                    }

                    //Remove first element in properties chain
                    properties = properties.Where((x, idx) => idx != 0).ToArray();

                    //Form left child properties into format Parent.Child.GrandChild
                    string leftProperties = string.Join(".", properties);

                    //Recursive call
                    FillObject(leftProperties, propertyInstance, sourceValue);
                }
                else //No more child property
                    CastAndSetValue(propertyInfo, targetInstance, sourceValue);
            }
            catch (InvalidCastException castException)
            {
                mappingExceptions.Add(new MappingException
                {
                    Message = "Mapping exception occurs while sets value to target property: " + property,
                    InnerException = castException
                });
            }
            catch (ArgumentException argumentException)
            {
                mappingExceptions.Add(new MappingException
                {
                    Message = "Mapping exception occurs while sets value to target property: " + property,
                    InnerException = argumentException
                });
            }
            catch (NullReferenceException nullException)
            {
                mappingExceptions.Add(new MappingException
                {
                    Message = "Mapping exception occurs while sets value to target property: " + property,
                    InnerException = nullException
                });
            }
            catch (Exception ex)
            {
                mappingExceptions.Add(new MappingException
                {
                    Message = "Mapping exception occurs while sets value to target property: " + property,
                    InnerException = ex
                });
            }
        }

        /// <summary>
        /// This method is used to get value of an instance's property
        /// </summary>
        /// <param name="instance">Object instance</param>
        /// <param name="propertyName">Property which value will be taken. Would be able to have the form of "Parent.Child.GrandChild"</param>
        /// <returns>Desirable value</returns>
        public virtual object GetValue(object instance, string propertiesChain)
        {
            if (instance == null && string.IsNullOrEmpty(propertiesChain))
                return null;

            string[] properties = propertiesChain.Split('.');
            string property = properties[0];

            try
            {
                PropertyInfo propertyInfo = instance.GetType().GetProperty(property);
                object propertyInstance = propertyInfo.GetValue(instance, null);

                if (properties.Length > 1) //It has child property
                {
                    //Remove first element in properties chain
                    properties = properties.Where((x, idx) => idx != 0).ToArray();

                    //Form left child properties into format Parent.Child.GrandChild
                    string leftProperties = string.Join(".", properties);

                    //Recursive call
                    return GetValue(propertyInstance, leftProperties);
                }
                else //No more child property
                    return propertyInstance;
            }
            catch (Exception ex)
            {
                mappingExceptions.Add(new MappingException
                {
                    Message = "Mapping exception occurs while gets value from source property: " + property,
                    InnerException = ex
                });
            }
            return null;                               
        }

        /// <summary>
        /// Cast and set value to target property
        /// </summary>
        /// <param name="info"></param>
        /// <param name="instance"></param>
        /// <param name="value"></param>
        protected void CastAndSetValue(PropertyInfo info, object instance, object value)
        {
            var destType = info.PropertyType.IsNullableType() ? Nullable.GetUnderlyingType(info.PropertyType) : info.PropertyType;
            var convertedValue = Convert.ChangeType(value, destType);

            info.SetValue(instance, convertedValue, null);
        }


    }
}
